﻿
using System;
using System.Diagnostics;
using System.Linq;
using System.Reflection;
using System.Runtime.CompilerServices;

using alib.Dictionary;
using alib.Hashing;
using alib.Debugging;
using alib.Collections;
using alib.Reflection;
using alib.Array;
using alib.file;

namespace agree.schema
{
	using xaml;

	public class PropertyChangeCallbackAttribute : Attribute { }
	public class NewObjectCallbackAttribute : Attribute { }

	///////////////////////////////////////////////////////////////////////////////////////////////////////////////////////
	/// 
	///////////////////////////////////////////////////////////////////////////////////////////////////////////////////////
	public sealed class Schema
	{
		public static readonly Schema Instance;

		public const String s_core_asm = "clr-namespace:agree;assembly=agree-core";

		public static assembly_stats[] rg_astats;

		static Schema()
		{
			var sch = new Schema();

#if DEBUG
			var astats_core = new assembly_stats(Assembly.GetExecutingAssembly(), true);
#else
			var astats_core = new assembly_stats(Assembly.GetExecutingAssembly(), false);
#endif

			sch.AddTypes(astats_core);

			sch.AddProperties(astats_core, typeof(PP));

			Instance = sch;
		}

		Schema()
		{
			this.sys_type_index = new IndexedHash<Type>(700);

			this.type_names = new StringIndex(sys_type_index.Capacity);

			this.constructed_generics = new IndexedHash<Type>(100);

			this.gx_by_id = new schema_type_generic[20];

			this.xsch = new xaml_schema(this);
		}

		public readonly xaml_schema xsch;

		public void Realize()
		{
			lock (this)
			{
				if (type_by_ix == null)
				{
					initialize_types();

					load_attached_properties();

					load_extension_methods();
				}
			}
		}

		///////////////////////////////////////////////////////////////////////////////////////////////////////////////////////
		/// 
		///////////////////////////////////////////////////////////////////////////////////////////////////////////////////////

		SlotDictionary<String, schema_prop_attachable> ap_map;

		schema_prop_attachable[] all_attached;

		//Type[] ap_pending;

		public void AddProperties(assembly_stats asc, Type _props)
		{
			if (asc.assigned_index == -1)
				asc.assigned_index = arr.Append(ref rg_astats, asc);

			asc.attached_property_set = _props;
			//arr.Append(ref ap_pending, _props);
		}

		[DebuggerBrowsable(DebuggerBrowsableState.Never)]
		public schema_prop_attachable[] AllAttached
		{
			get
			{
				schema_prop_attachable[] _tmp;
				if ((_tmp = all_attached) == null)
				{
					Realize();
					_tmp = all_attached;
				}
				return _tmp;
			}
		}

		public schema_prop_attachable GetAttachedProp(String s_name)
		{
			SlotDictionary<String, schema_prop_attachable> _tmp;
			if ((_tmp = ap_map) == null)
			{
				Realize();
				_tmp = ap_map;
			}
			schema_prop_attachable ap;
			var q = _tmp.TryGetValue(s_name, out ap) ? ap : default(schema_prop_attachable);
			return q;
		}

		public xaml_attached GetXamlAttachedProp(String s_name)
		{
			schema_prop_attachable spa;
			return (spa = GetAttachedProp(s_name)) != null ? spa.XamlAttachedProperty : default(xaml_attached);
		}

		void init_ap_maps(int c)
		{
			//Debug.Print("initialize " + c + " attached properties from " + rg_astats.Length + " schemas...");
			this.all_attached = new schema_prop_attachable[c];
			this.ap_map = new SlotDictionary<String, schema_prop_attachable>(c, StringComparer.Ordinal);
		}

		void load_attached_properties()
		{
			int c, i;
			if (rg_astats == null || (c = rg_astats.Length) == 0)
			{
				init_ap_maps(0);
				return;
			}

			var rgrgfi = new FieldInfo[c][];
			FieldInfo[] rgfi;
			int c_att = 0;
			for (i = 0; i < c; i++)
			{
				var taps = rg_astats[i].attached_property_set;
				if (taps != null && c_att < (c_att += (rgfi = taps.GetFields(BindingFlags.Static | BindingFlags.Public)).Length))
					rgrgfi[i] = rgfi;
			}

			init_ap_maps(c_att);

			c_att = 0;
			for (i = 0; i < c; i++, c_att += rgfi == null ? 0 : rgfi.Length)
				if ((rgfi = rgrgfi[i]) != null)
					load_attached_props(rg_astats[i], c_att, rgfi);
		}

		void load_attached_props(assembly_stats asc, int i, FieldInfo[] rgfi)
		{
			Assembly asm = asc.asm;
			FieldInfo fi;

			Debug.Assert(asc.asm == rgfi[0].DeclaringType.Assembly);

			for (int j = 0; j < rgfi.Length; j++)
			{
				var s_name = (fi = rgfi[j]).Name;

				var Ti_direct = asm.GetType("agree.schema.ρρ" + s_name);

				var pi_direct = Ti_direct.GetProperties()[0];

				if (pi_direct.Name != s_name)
					throw new Exception();

				var aa = all_attached[i++] = new schema_prop_attachable(this, pi_direct, Ti_direct);

				fi.SetValue(null, aa);

				ap_map.Add(s_name, aa);
			}
			asc.PropsCount = rgfi.Length;
		}

		void load_extension_methods()
		{
			int c, i;
			if (rg_astats == null || (c = rg_astats.Length) == 0)
			{
				return;
			}

			for (i = 0; i < c; i++)
			{
				var taps = rg_astats[i].attached_property_set;
				if (taps == null)
					continue;
				MethodInfo[] rgmi = taps.GetMethods(BindingFlags.Static | BindingFlags.Public);
				for (int j = 0; j < rgmi.Length; j++)
				{
					var mi = rgmi[j];

					var attr = mi.CustomAttributes.Select(cad => cad.AttributeType).ToArray();
					if (Array.IndexOf<Type>(attr, typeof(PropertyChangeCallbackAttribute)) != -1)
					{
						Debug.Assert(mi.ReturnType == typeof(void));

						var px = mi.GetParameters();
						Debug.Assert(px.Length == 3 && px[1].ParameterType == typeof(schema_prop) && px[2].ParameterType == typeof(Object));

						var t_this = px[0].ParameterType;
						Debug.Assert(typeof(ISchemaObj).IsAssignableFrom(t_this));

						var arr1 = new[] { mi };

						schema_type_known stk;
						for (int k = 0; k < type_by_id.Length; k++)
							if (t_this.IsAssignableFrom(stk = type_by_id[k]))
								stk.AddPropchangeCallback(arr1);
					}
					else if (Array.IndexOf<Type>(attr, typeof(NewObjectCallbackAttribute)) != -1)
					{
						var px = mi.GetParameters();

						Debug.Assert(mi.ReturnType == typeof(void) && px.Length == 2 && px[0].ParameterType == typeof(schema_type));

						var t_this = px[1].ParameterType;
						Debug.Assert(typeof(ISchemaObj).IsAssignableFrom(t_this));

						var arr1 = new[] { mi };

						schema_type_known stk;
						for (int k = 0; k < type_by_id.Length; k++)
							if (t_this.IsAssignableFrom(stk = type_by_id[k]))
								stk.AddNewObjCallback(arr1);
					}
					else
					{
						var px = mi.GetParameters();

						Type t_this;
						if (px.Length == 0 || !typeof(ISchemaObj).IsAssignableFrom(t_this = px[0].ParameterType))
							throw new Exception();

						schema_type_known stk;
						for (int k = 0; k < type_by_id.Length; k++)
							if (t_this.IsAssignableFrom(stk = type_by_id[k]))
								stk.AddExtension(mi);
					}
				}
			}
		}

		public IndexedHash<Type> sys_type_index;

		public schema_type_known[] type_by_ix, type_by_id;

		public StringIndex type_names;

		public IndexedHash<Type> constructed_generics;

		public schema_type_generic[] gx_by_id;

		///////////////////////////////////////////////////////////////////////////////////////////////////////////////////////
		/// 
		///////////////////////////////////////////////////////////////////////////////////////////////////////////////////////
		bool intercept(Type t)
		{
#if DEBUG
			if (t is schema_type)
				throw new Exception();
#endif
			if (!typeof(ISchemaObj).IsAssignableFrom(t))
				return false;

			String ns;
			if (String.IsNullOrEmpty(ns = t.Namespace) || !ns.StartsWith("agree") || ns.StartsWith("agree.schema"))
				return false;

			if (t.Name.Contains("__"))
				return false;

			if (t.IsArray || t.IsEnum || t.IsGenericParameter)
				return false;

			if (t.IsGenericType)
				return true;

			if (t.CustomAttributes.Any(cad => cad.AttributeType == typeof(ExtensionAttribute)))
				return false;

			return true;
		}

		///////////////////////////////////////////////////////////////////////////////////////////////////////////////////////
		/// 
		///////////////////////////////////////////////////////////////////////////////////////////////////////////////////////
		public void AddTypes(assembly_stats asc)
		{
			asc.assigned_index = arr.Append(ref rg_astats, asc);

			var rgT = asc.asm.GetTypes();

			int c_pre = sys_type_index.Count;

			Type t;
			for (int j = 0; j < rgT.Length; j++)
				if (intercept(t = rgT[j]) && sys_type_index.Add(t) != type_names.Add(t.FullName))
					throw new Exception();

			asc.TypesCount = sys_type_index.Count - c_pre;

			//Debug.Print("add {0} types from {1}", sys_type_index.Count - c_pre, a.FullName);
		}

		///////////////////////////////////////////////////////////////////////////////////////////////////////////////////////
		/// 
		///////////////////////////////////////////////////////////////////////////////////////////////////////////////////////
		void initialize_types()
		{
			//foreach (var zz in sys_type_index)
			//	Debug.Print("{0}.{1}", zz.Namespace, zz._Name());

			int c_types = sys_type_index.Count;
			var arr = sys_type_index.GetItemsArray();

			var by_ix = new schema_type_known[c_types];
			var by_id = new schema_type_known[c_types];

			int i;
			for (i = 0; i < c_types; i++)
			{
				var t = arr[i];
				schema_type_known st;
				if (t.IsGenericTypeDefinition)
					st = new schema_type_generic_def(this, arr[i], c_types, i);
				else
					st = new schema_type_norm(this, arr[i], c_types, i);
				by_id[i] = by_ix[i] = st;
			}

			schema_type_norm stn;
			for (i = 0; i < c_types; i++)
				if ((stn = by_ix[i] as schema_type_norm) != null)
					stn.init_phase_two();

			Array.Sort<schema_type_known>(by_id);

			for (i = 0; i < c_types; i++)
				by_id[i].id = i;

			this.type_by_id = by_id;
			this.type_by_ix = by_ix;

			if (c_types != -1)
				Debug.Print("initialized " + c_types + " types...");
		}

		///////////////////////////////////////////////////////////////////////////////////////////////////////////////////////
		/// 
		///////////////////////////////////////////////////////////////////////////////////////////////////////////////////////

		public schema_type find_type(Type t)
		{
			schema_type st;
			if ((st = t as schema_type) != null)
				return st;

			if (t.IsConstructedGenericType)
				return get_generic_type(t);

			schema_type_known[] _tmp;
			int ix;
			if ((ix = sys_type_index[t]) == -1)
			{
				var msg = String.Format("Unindexed type: {0}", t._Name());
				Debug.WriteLine(msg);
				throw new Exception(msg);
			}

			if ((_tmp = type_by_ix) == null)
			{
				Realize();
				_tmp = type_by_ix;
			}

			if (ix >= _tmp.Length)
				throw new Exception("Types added after schema was initialized");

			return _tmp[ix];
		}

		public Type[] promote_types(Type[] rgt)
		{
			for (int i = 0; i < rgt.Length; i++)
				promote_type(ref rgt[i]);
			return rgt;
		}

		public void promote_type(ref Type t) { t = promote_type(t); }

		public Type promote_type(Type t)
		{
			if (t == null || t is schema_type || !intercept(t))
				return t;

			if (t.IsConstructedGenericType)
				return get_generic_type(t);

			schema_type_known[] _tmp;
			int ix;
			if ((ix = sys_type_index[t]) == -1)
				throw new Exception();

			if ((_tmp = type_by_ix) == null)
			{
				Realize();
				_tmp = type_by_ix;
			}

			if (ix >= _tmp.Length)
				throw new Exception("Types added after schema was initialized");

			return _tmp[ix];
		}

		schema_type_generic get_generic_type(Type t)
		{
			int ix;
			schema_type_generic[] _tmp;
			schema_type_generic st;
			lock (constructed_generics)
			{
				if ((ix = constructed_generics.Add(t)) >= (_tmp = gx_by_id).Length)
					_tmp = gx_by_id = arr.Resize(_tmp, constructed_generics.Capacity);
				else if ((st = _tmp[ix]) != null)
					return st;
				st = _tmp[ix] = new schema_type_generic(this, t, ix);
			}
			return st;
		}
		public schema_type get_type(Object obj)
		{
			return find_type(obj.GetType());
		}
		public schema_type _typeof<T>()
		{
			return find_type(typeof(T));
		}
		public T promote_to<T>(Type t)
			where T : schema_type
		{
			return promote_type(t) as T;
		}

		///////////////////////////////////////////////////////////////////////////////////////////////////////////////////////
		/// 
		///////////////////////////////////////////////////////////////////////////////////////////////////////////////////////
		public item_property_value[] PropertyValues(ISchemaObj item)
		{
			schema_prop_attachable[] _tmp = this.AllAttached;
			int i, j = 0, c = _tmp.Length;
			item_property_value pi = default(item_property_value);
			while (j < c)
				if (pi.try_get(item, _tmp[j++]))
					goto first;
			return Collection<item_property_value>.None;

		first:
			item_property_value[] arr;
			(arr = new item_property_value[_tmp.Length])[0] = pi;
			i = 1;
			while (j < c)
				if (arr[i].try_get(item, _tmp[j++]))
					i++;
			if (i < c)
			{
				item_property_value[] ret = new item_property_value[i];
				Array.Copy(arr, ret, i);
				arr = ret;
			}
			return arr;
		}

		///////////////////////////////////////////////////////////////////////////////////////////////////////////////////////
		/// 
		///////////////////////////////////////////////////////////////////////////////////////////////////////////////////////
		public void CloneProperties(ISchemaObj src, ISchemaObj dst)
		{
			schema_prop_attachable[] _tmp = this.AllAttached;
			int c = _tmp.Length;
			schema_prop_attachable spa;
			Object value;

			for (int i = 0; i < c; i++)
				if ((value = (spa = _tmp[i]).GetValue(src)) != null)
					spa.SetValue(dst, value);
		}
	};
}
